// Copyright (c) 2009 - Quildreen <quildreen@gmail.com>                     
//                                                                          
//  Permission is hereby granted, free of charge, to any person              
//  obtaining a copy of this software and associated documentation files     
//  (the "Software"), to deal in the Software without restriction,           
//  including without limitation the rights to use, copy, modify, merge,     
//  publish, distribute, sublicense, and/or sell copies of the Software,     
//  and to permit persons to whom the Software is furnished to do so,        
//  subject to the following conditions:                                     
//                                                                           
//  The above copyright notice and this permission notice shall be           
//  included in all copies or substantial portions of the Software.          
//                                                                           
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,          
//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF       
//  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND                    
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE   
//  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION   
//  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION    
//  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
/**
 * lily.input.core
 *
 * Abstracts input in javascript.
 **/
lily.defineModule("input.core");


/**
 * lily.input.constants
 * 
 * Defines some global constants for the input package.
 **/
lily.defineNamespace("lily.input.constants");
Object.extend(lily.input.constants, {
    ST_NONE:   0,
    ST_DOWN:   1,
    ST_HELD:   2,
    ST_UP:     3,
    
    // mouse buttons
    MB_LEFT:   1,
    MB_MIDDLE: 2,
    MB_RIGHT:  3,
    
    // key codes
    /* control characters */
    K_BACKSPACE: 8,    /* backspace */
    K_TAB: 9,          /* horizontal tab */
    K_RETURN: 13,      /* return key */
    K_SHIFT: 16,       /* shift (both left and right) */
    K_CTRL: 17,        /* control (both left and right) */
    K_ALT: 18,         /* alt (both left and right) */
    K_PAUSE: 19,       /* pause */
    K_ESCAPE: 27,      /* escape */
    K_SPACE: 32,       /* space bar */
    K_PAGEUP: 33,      /* page up */
    K_PAGEDOWN: 34,    /* page down */
    K_END: 35,         /* end */
    K_HOME: 36,        /* home */

    K_CAPS: 20,        /* caps lock */
    K_NUMLOCK: 144,    /* numlock */
    K_SCROLL: 145,     /* scroll lock */

    /* arrows */
    K_LEFT: 37,        /* arrow left */
    K_UP: 38,          /* arrow up */
    K_RIGHT: 39,       /* arrow right */
    K_DOWN: 40,        /* arrow down */

    K_INSERT: 45,      /* insert */
    K_DELETE: 46,      /* delete */

    /* numbers */
    K_0: 48,           /* 0 */
    K_1: 49,           /* 1 */
    K_2: 50,           /* 2 */
    K_3: 51,           /* 3 */
    K_4: 52,           /* 4 */
    K_5: 53,           /* 5 */
    K_6: 54,           /* 6 */
    K_7: 55,           /* 7 */
    K_8: 56,           /* 8 */
    K_9: 57,           /* 9 */ 

    /* letters */
    K_A: 65,           /* a */
    K_B: 66,           /* b */
    K_C: 67,           /* c */
    K_D: 68,           /* d */
    K_E: 69,           /* e */
    K_F: 70,           /* f */
    K_G: 71,           /* g */
    K_H: 72,           /* h */
    K_I: 73,           /* i */
    K_J: 74,           /* j */
    K_K: 75,           /* k */
    K_L: 76,           /* l */
    K_M: 77,           /* m */
    K_N: 78,           /* n */
    K_O: 79,           /* o */
    K_P: 80,           /* p */
    K_Q: 81,           /* q */
    K_R: 82,           /* r */
    K_S: 83,           /* s */
    K_T: 84,           /* t */
    K_U: 85,           /* u */
    K_V: 86,           /* v */
    K_W: 87,           /* w */
    K_X: 88,           /* x */
    K_Y: 89,           /* y */
    K_Z: 90,           /* z */

    K_LSUPER: 91,      /* left windows key */
    K_RSUPER: 92,      /* right windows key */

    /* keypad */
    K_KP0: 96,         /* keypad 0 */
    K_KP1: 97,         /* keypad 1 */
    K_KP2: 98,         /* keypad 2 */
    K_KP3: 99,         /* keypad 3 */
    K_KP4: 100,        /* keypad 4 */
    K_KP5: 101,        /* keypad 5 */
    K_KP6: 102,        /* keypad 6 */
    K_KP7: 103,        /* keypad 7 */
    K_KP8: 104,        /* keypad 8 */
    K_KP9: 105,        /* keypad 9 */
    K_KP_MUL: 106,     /* keypad * */
    K_KP_ADD: 107,     /* keypad + */
    K_KP_SUB: 109,     /* keypad - */
    K_KP_DOT: 110,     /* keypad . */
    K_KP_DIV: 111,     /* keypad / */
    
    /* function keys */
    K_F1: 112,         /* f1 */
    K_F2: 113,         /* f2 */
    K_F3: 114,         /* f3 */
    K_F4: 115,         /* f4 */
    K_F5: 116,         /* f5 */
    K_F6: 117,         /* f6 */
    K_F7: 118,         /* f7 */
    K_F8: 119,         /* f8 */
    K_F9: 120,         /* f9 */
    K_F10: 121,        /* f10 */
    K_F11: 122,        /* f11 */
    K_F12: 123         /* f12 */
});


/**
 * lily.input.Manager
 * 
 * Abstracts common input through keyboard and mouse into a object that can
 * be pooled for states.
 **/
lily.defineNamespace("lily.input.Manager");
Object.extend(lily.input.Manager, (function()
{
    var Man = lily.input.Manager;
    var c = lily.input.constants;
    var _last_position = [0, 0];
    
    
    /**
     * lily.input.Manager.addEvent(event) -> undefined
     * - event (Object): the event to add.
     * 
     * Adds a event to the state list.
     * 
     * Events are plain JavaScript objects with some key properties set.
     * 
     * __Properties__
     * 
     * + `type` (String): the event type. Can be one of the following:
     *    
     *   - `quit` 
     *   - `active`
     *   - `key_down`
     *   - `key_up`
     *   - `mouse_motion`
     *   - `mouse_button_down`
     *   - `mouse_button_up`
     *   - `mouse_wheel`
     *   - `custom`
     * 
     * + `key` (Number | String): When defining a keyboard event, this refers
     *    to the code of the key for which the event is being set. For user
     *    events, however, this gives the custom event an unique indentifier,
     *    that can be polled later (with `"USR_" + key`)
     * 
     * + `delta` (Number): Mouse wheel movement delta.
     * 
     * + `state` (Number): State of the object to store in the custom event.
     **/
    function add_event(event)
    {        
        switch (event.type) {
            case "quit":
                Man.state["quit"] = {event: event};
                break;
            case "active":
                Man.state["active"] = {event: event};
                break;
                
            // keyboard events
            case "key_down":
                if (!Man.state[event.key] || (Man.state[event.key]
                    && Man.state[event.key].state != c.ST_HELD)) {
                    Man.state[event.key] = {event: event, state: c.ST_DOWN};
                }
                break;
            case "key_up":
                Man.state[event.key] = {event: event, state: c.ST_UP};
                break;
            
            // mouse events
            case "mouse_motion":
                Man.mouse_x = event.ev.pointer[0];
                Man.mouse_y = event.ev.pointer[1];
                break;
            case "mouse_button_down":
                Man.state["MB_" + event.button] = {event: event,
                        state: c.ST_DOWN};
                break;
            case "mouse_button_up":
                Man.state["MB_" + event.button] = {event: event,
                        state: c.ST_UP};
                break;
            case "mouse_wheel":
                Man.wheel_delta += event.delta;
                break;
            
            // user events
            case "custom":
                Man.state["USR_" + event.key] = {event: event, 
                        state: event.state};
                break;
        }
    }
    

    /**
     * lily.input.Manager.isSet(key) -> Boolean
     * - key (Number | String): the event key to fetch. 
     * 
     * Returns whether a state for a key is set or not.
     **/
    function isSet(key)
    {
        return !(!Man.state[key]);
    }


    /**
     * lily.input.Manager.isUp(key) -> Boolean
     * - key (Number | String): the event key to check.
     * 
     * Returns whether a key is being released in this frame or not.
     **/
    function isUp(key)
    {
        return Man.state[key] && Man.state[key].state == c.ST_UP;
    }


    
    /**
     * lily.input.Manager.isDown(key) -> Boolean
     * - key (Number | String): the event key to check.
     * 
     * Returns whether a key is being pressed in this frame or not.
     **/
    function isDown(key)
    {
        return Man.state[key] && Man.state[key].state == c.ST_DOWN;
    }


    /**
     * lily.input.Manager.isHeld(key) -> Boolean
     * 
     * Returns whether a key is being held in this frame or not.
     **/
    function isHeld(key)
    {
        return Man.state[key] && (Man.state[key].state == c.ST_HELD
                                  || Man.state[key].state == c.ST_DOWN);
    }
    
    
    /**
     * lily.input.Manager.update() -> undefined
     * 
     * Updates the key states. Should be called once per frame.
     **/
    function update()
    {
        var states = Man.state;
        
        for (key in states) {
            var s = states[key].state;
            if (s == c.ST_DOWN) {
                states[key].state = c.ST_HELD;
            } else if (s == c.ST_UP) {
                delete states[key];
            }
        }

        
        // calculates mouse positions
        Man.mouse_x = Man.mouse_x - Man.offset[0];
        Man.mouse_y = Man.mouse_y - Man.offset[1];
        Man.mouse_delta = [Man.mouse_x - _last_position[0],
                            Man.mouse_y - _last_position[1]];
        _last_position = [Man.mouse_x, Man.mouse_y];
    }
    
        
    /**
     * lily.input.Manager.install([window]) -> undefined
     * - window (HTMLElement | String): the game window DOM element's
     *   instance or ID. 
     * 
     * Install the object as the default handler for the document events.
     **/
    function install(window)
    {
        document.observe('keydown', function(ev) {
            ev.stop();
            Man.addEvent({ev: ev,
                          type: "key_down",
                          key: ev.keyCode
                         });
        });
        document.observe('keyup', function(ev) {
            ev.stop();
            Man.addEvent({ev: ev,
                          type: "key_up",
                          key: ev.keyCode
                         });
        });
        document.observe('mousemove', function(ev) {
            var p = ev.pointer();
            Man.mouse_x = p.x;
            Man.mouse_y = p.y;
        });
        document.observe('mousedown', function(ev) {
            ev.stop();
            
            // todo: Add support for IE and its non-standard buttons?
            var button = ev.which || (ev.button + 1);
            
            Man.addEvent({ev: ev,
                          type: 'mouse_button_down',
                          button: button
                         });
        });
        document.observe('mouseup', function(ev) {
            ev.stop();
            
            // todo: Add support for IE and its non-standard buttons?
            var button = ev.which || (ev.button + 1);
            
            Man.addEvent({ev: ev,
                          type: 'mouse_button_up',
                          button: button
                          });
        });
        
        // handles mouse wheel inconsistencies and adjusts its delta
        // todo: write a custom event for mouse wheel events?
        function wheel(ev) {
            var delta;
            if (event.wheelDelta) {
                delta = event.wheelDelta / 120;
            } else if (event.detail) {
                delta = -event.detail / 3;
            }
            
            if (delta) {
                Man.wheel_delta += delta;
            }
            
            // prevent the actual scroll from ocurring.
            ev.preventDefault && ev.preventDefault();
            ev.returnValue = false;
        }
        
        document.observe('mousewheel', wheel);
        document.observe('DOMMouseScroll', wheel);

        window && (window = $(window));
        if (window) {
            Man.window = window;
        }
        if (Man.window) {
            Man.offset = Man.window.cumulativeOffset();
        }
    }
    
    return {
        /**
         * lily.input.Manager.window -> HTMLElement
         * 
         * The game's window, for which positions should be calculated.
         **/
        window: $$("body")[0],
        
        /**
         * lily.input.Manager.state -> Object
         * 
         * An object containing the current input states for keys/mouse.
         **/
        state: {},
                
        /**
         * lily.input.Manager.mouse_x -> Number
         * 
         * Current mouse x position, relative to the left of the window.
         **/
        mouse_x: 0,
        
        /**
         * lily.input.Manager.mouse_y -> Number
         * 
         * Current mouse y position, relative to the top of the window.
         **/
        mouse_y: 0,
        
        /**
         * lily.input.Manager.mouse_delta -> Array
         * 
         * The amount of movement of the mouse wheel in this frame.
         **/
        mouse_delta: [0, 0],
        
        /**
         * lily.input.Manager.wheel_delta -> Number
         * 
         * The amount of movement of the mouse wheel, between the last frame
         * and the current one.
         **/
        wheel_delta: 0,

        /**
         * lily.input.Manager.offset -> Array
         * 
         * The the game's container element offset relative to the top-left of
         * the page.
         **/
        offset: [0, 0],
        
        /* methods */
        addEvent: add_event,
        isSet: isSet,
        isUp: isUp,
        isDown: isDown,
        isHeld: isHeld,
        update: update,
        install: install
    };
})());